Skip to content

feat(skills): rework payments around the placed-cart lock, add Mollie and Two - #26

Merged
Plopix merged 6 commits into
mainfrom
feat/payments-skill-rework
Oct 6, 2026
Merged

Plopix merged 6 commits into
mainfrom
feat/payments-skill-rework

Conversation

@Plopix

@Plopix Plopix commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Reworks the payments skill from the crystallize.com payment-gateway docs, without the deprecated Node request-handler library.

SKILL.md

  • The common flow from the docs: place the cart, create the provider session, wait for the webhook, then create the order.
  • Lock the cart before you charge. The page explains the two-tab attack and how place prevents it: createFromCart refuses a cart that isn't placed. It also explains that place re-prices the cart, and that writes to a placed cart are silently dropped.
  • One payment per cart. Each provider's idempotency mechanism keeps one session per cart. A second payment that still gets through is recorded on the order and flagged for refund, never turned into a second order.
  • The webhook skeleton. Verify over the raw body, re-fetch when the notification is unsigned, then createOrderOnce:
    • takes the Shop API /lock;
    • creates the Core customer if it's missing;
    • calls createFromCart once;
    • waits until the cart shows ordered.
  • Read-only return page. It takes the cart id from the URL and asks the provider for the payment's status while the cart is still placed.
  • Generic Shop API payment records for every provider. paymentStatus is set once, at creation.
  • Capture and cancel from fulfilment pipeline stages. A Crystallize webhook, verified with createSignatureVerifier, calls the provider, then setPayments updates the record.

References

  • All 9 existing references were rewritten and re-verified against the providers' current docs (2026-10-06).
  • Each one now includes what its crystallize.com page says.
  • Provider-specific features from the old boilerplates are kept as plain provider API calls: Montonio bank picker and parcel machines, Vipps QR/app-switch, receipts and Login, Klarna's category chooser, Qliro's extras, and others.
  • New references: Mollie (Payments API) and Two (B2B invoice).
  • Every code block compiles under tsc --strict against @crystallize/js-api-client 7.4 and the providers' SDKs. Nothing was run end-to-end against a provider.

How it was tested

Following the writing-skills process, each scenario was run without the skill first, then with it, and the written answers were compared. Nothing was executed.

  • Without the skill (Stripe checkout, QuickPay callback, Klarna capture/refund), the answers missed:
    • the concurrent-webhook lock;
    • creating the Core customer;
    • the silently dropped writes on a placed cart;
    • and they wrote typed Klarna payments through Core updateOrder.
  • With the skill, all three closed those gaps. So did four more scenarios: Mollie, Two, Montonio parcel machines, and the Vipps two-tab bug.

Needs a decision on the platform side

  • Shop /order can't change paymentStatus after creation. addPayments and setPayments take only id and payments. Patching it through Core updateOrder pushes Core's whole order back to the Shop store, which can overwrite the payments just set. So the skill leaves paymentStatus as created, and the payment record's meta.state tracks capture and refund. A setPaymentStatus mutation on Shop /order would fix this.
  • The crystallize.com Shop API orders page says to "include paymentStatus alongside your payment updates". The API doesn't support that.

Not done

  • The live reference-selection eval (bun run bin/eval-reference-selection.ts) failed with a Workers AI authentication error. Wrangler is logged in to a different Cloudflare account than the one pinned in wrangler.jsonc. The Mollie and Two cases are added and ready to run.

Requires @crystallize/js-api-client 7.5.0 (published)

The shared helper uses createShopOrderManager, createShopCustomerManager and createShopLock from @crystallize/js-api-client 7.5.0, which builds on @crystallize/schema 6.15.0. Both are now on npm. Every code block in payments (SKILL.md and the 11 references) and in js-api-client was re-checked with tsc --strict against the published packages.

The Shop API /lock is opt-in, under "Serialising order writes (optional)". It is never used by default or in the Pay route.

js-api-client and mutation skills (consequences of 7.5)

  • js-api-client:
    • The five new shop*Api callers.
    • The shared Shop API token and its scopes.
    • createShopOrderManager, createShopCustomerManager and createShopLock, with their gotchas: meta is read back as an object, writes persist just after the response, and optional fields reject null.
    • The helpers table now marks each one Core or Shop, with guidance on when to use createOrderManager (Core) vs createShopOrderManager (Shop).
    • Tested before and after the edit: without it, an agent recommended Core createOrderManager for a cart-based order. With it, the agent answered all five Shop API questions correctly.
  • mutation: the JS snippet now uses nextPimApi for Core instead of the legacy pimApi, and shows the Shop API callers.
  • payments: the description is trimmed under the 1024-character skill limit.

Plopix added 6 commits October 6, 2026 14:23
… and Two

- SKILL.md: the common flow from the crystallize.com docs, why the cart is placed
  before charging (the two-tab attack), one payment per cart, the webhook skeleton,
  a read-only return page, shared Shop API helpers (/lock, createOrderOnce, Core
  customer, recordPayment, updatePayment), paymentStatus set once at creation,
  and capture/cancel from fulfilment pipeline stages.
- Every provider reference re-verified against current provider docs (2026-10-06),
  with the crystallize.com page content folded in and provider-specific features
  kept as plain API calls.
- New references: Mollie (Payments API) and Two (B2B invoice).
- No dependency on the deprecated request-handler library or old boilerplates.
A skill page inlines SKILL.md and its references/*.md into one document, but
the relative .md links between those files were rendered as-is, so links like
references/plans-and-pricing.md 404ed. Each reference now gets an anchor above
its section, and links to a reference, a SKILL.md or another skill's files are
rewritten to the page and anchor they ended up on.
createOrderOnce, recordPayment and updatePayment no longer take the Shop API
lock, and the Dintero, Qliro and Two pay routes no longer hold it in the
shopper's path. A new 'Serialising order writes (optional)' section explains
what colliding deliveries can lose without it, what it costs, and when to opt in.
The helper intro now says why shop() exists: shopCartApi only reaches /cart.
lib/crystallize-payments.ts drops the hand-rolled shopToken()/shop(): orders go
through createShopOrderManager, the 'create the customer if missing' step through
createShopCustomerManager (fetch, then upsert only when absent, nulls stripped),
and the opt-in lock through createShopLock. One client token covers the cart,
order and customer scopes.
- js-api-client: the five new shop*Api callers, the shared Shop API token and its
  scopes, createShopOrderManager, createShopCustomerManager and createShopLock with
  their gotchas (meta read back as an object, writes persist after the response,
  optional fields reject null), Core vs Shop helpers marked in the table, and when
  to use createOrderManager (Core) vs createShopOrderManager (Shop).
- mutation: the JS snippet uses nextPimApi for Core and shows the Shop API callers.
- payments: description trimmed under the 1024-character skill limit.
@Plopix
Plopix merged commit dd04ffd into main Oct 6, 2026
1 check passed
@Plopix
Plopix deleted the feat/payments-skill-rework branch October 6, 2026 23:28
Plopix added a commit that referenced this pull request Oct 7, 2026
Deploy Docs only starts when a CI push run on main completes. The merge of
#26 never got a CI push run (GitHub Webhooks were degraded at the time) and
the release commit is pushed with GITHUB_TOKEN, which triggers nothing, so
the docs stayed on f8b6405 without the skill-link rewrite. A
workflow_dispatch trigger, limited to main, gives a way to deploy anyway.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant